<?php

require_once 'PipesImpl/Model/Meta.php';
require_once 'PipesImpl/Model.php';
require_once 'PipesImpl/Namespace.php';
require_once 'Pipes/Exception.php';
require_once 'Pipes/Value/Callback.php';

abstract class Pipes_Model implements PipesImpl_Model
{
    /**
     *
     * An array of properties that this Model represents as returned by 
     * {@link __get()}.
     *
     *
     * @var array
     *
     * @access protected
     */
    private $_data = array();
    
    
    /**
     *
     * An array of all of the changes to the various properties
     *
     *
     * @var array
     *
     * @access protected
     *
     */
    private $_changes = array();
    
    
    /**
     *
     * A link to the {@link Domain51_Model_Meta} object that represents this
     * Model.
     *
     *
     * @var array
     *
     * @access protected
     *
     */
    private $_meta = null;
    
    
    /**
     *
     * A link to a valid callback that will be called on {@link save()}
     *
     *
     * @var mixed
     *
     * @access protected
     *
     * @see attach()
     *
     */
    private $_saveCallback = null;
    
    private $_relationships = array();
    
    /**
     *
     * Handle instantiation
     *
     * This is called by {@link Domain51_Model_Mapper} when a new Model is
     * fetched.
     *
     * @param array $config An associative array of data that represents this
     * model.
     *
     * @return void
     *
     */
    public function __construct(PipesImpl_Model_Meta $meta, $properties = array()) {
        $this->_meta = $meta;
        
        // populate _data based on meta object
        $columnData = $this->getColumns();
        $columnNames = array_keys($columnData);
        foreach ($columnNames as $value) {
            if (isset($properties[$value])) {
                $this->_data[$value] = $properties[$value];
            }
        }
    }
    
    
    /**
     *
     * Return the value of a given key.
     *
     * Since Model's keep track of changes without committing to persistant
     * storage, it is possible for this value to be out of sync with the 
     * persistant storage layer.  To see if this is a modified value, use
     * {@link isModified()} and specify the key
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>$model->key;</code>
     *
     * @param string $key
     *
     * @return void
     *
     */
    public function __get($key) {
        if (isset($this->_changes[$key])) {
            // return latest changed version of this
            return $this->_changes[$key];
        } elseif (isset($this->_data[$key])) {
            // return property as set at construct
            return $this->_data[$key];
        }
        
        // look for cached relationships
        if (isset($this->_relationships[$key])) {
            return $this->_relationships[$key];
        }
        
        // look for relationships that haven't been cached
        if ($this->_meta->hasRelationship($key)) {
            // strip "s" off end
            $realKey = substr($key, 0, -1);
            $this->_relationships[$key] = $this->_meta->getRelationship($this, $realKey);
            return $this->_relationships[$key];
        }
        
        // nothing returned
        return null;
    }
    
    
    /**
     *
     * Sets a given property to a specific value.
     *
     * Throws an exception when the specified $key is unknown.
     *
     * This is a PHP magic method, and is called directly by invoking:
     * <code>$model->key = "value";</code>
     *
     * In addition to known specific keys, if $key is the name of a relationship
     * that this Model has, it will be set as well.  It is assumed that any 
     * implementing model that uses this style of setting will implement the 
     * handling of this value by overriding {@link save()}.
     *
     *
     * @param string $key
     *
     * @param mixed $value
     *
     * @return void
     *
     */
    public function __set($key, $value) {
        $knownColumns = $this->getColumns();
        if (!isset($knownColumns[$key]) && !$this->_meta->hasRelationship($key)) {
            throw new Pipes_Exception("ERR_UNKNOWN_PROPERTY [$key]");
        }
        $this->_changes[$key] = $value;
    }
    
    
    /**
     *
     * Returns true if a given property is set
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>isset($model->key);</code>
     *
     *
     * @param string $key
     *
     * @return bool
     *
     */
    public function __isset($key) {
        return isset($this->_data[$key]) || isset($this->_changes[$key]);
    }
    
    
    /**
     *
     * Unset a changed property
     *
     * This will not unset properties from persistent storages.  To change the
     * type, you will need to actually set it to the type you wish to use.
     *
     * This is a PHP magic method and is called directly by invoking:
     * <code>unset($model->key);</code>
     *
     *
     * @param string $key
     *
     * @return void
     *
     */
    public function __unset($key) {
        unset($this->_changes[$key]);
    }
    
    /**
     *
     * Revert all changes to this property
     *
     *
     * @return void
     *
     */
    public function revert() {
        $this->_changes = array();
    }
    
    /**
     *
     * Returns an array of the latest data
     *
     *
     * @return array
     *
     */
    public function toArray() {
        return array_merge($this->_data, $this->_changes);
    }
    
    
    /**
     *
     * Return true if this Model has been modified.  If $key is provided, 
     * returns true only if that property has been modified.
     *
     *
     * @param string|null $key Specify which property to check if it is 
     *    modified.  Optional.
     *
     * @return bool
     *
     */
    public function isModified($key = null)
    {
        if (empty($key)) {
            return (count($this->_changes) > 0);
        } else {
            return isset($this->_changes[$key]);
        }
    }
    
    
    /**
     *
     * Return true if this Model is new
     *
     *
     * @return bool
     *
     */
    public function isNew()
    {
        return (count($this->_data) == 0);
    }
    
    
    /**
     *
     * Attaches a valid PHP callback to be invoked on {@link save()}
     *
     * Only one callback can be attached at a time.  See 
     * {@link http://www.php.net/manual/en/function.is-callable.php is_callable()}
     * for a definition of what a valid callback is.
     *
     *
     * @param mixed $callback A valid PHP callback.
     *
     * @return void
     *
     */
    public function attach($callback)
    {
        $this->_saveCallback = new Pipes_Value_Callback($callback);
    }
    
    
    /**
     *
     * Saves this Model.
     *
     * If a callback has been specified by {@link attach()}, that callback is
     * invoked with this object as the only parameter.
     *
     * On save(), all changed data is persisted, and unset() will no longer 
     * revert changes.
     *
     *
     * @return void
     *
     */
    public function save()
    {
        if (!is_null($this->_saveCallback) && $this->isModified()) {
            $updateChanges = (array)call_user_func($this->_saveCallback->value, $this);
            $this->_changes = array_merge($this->_changes, $updateChanges);
        }

        // always call save() on any loaded relationships and let them determine
        // if they should do anything
        foreach ($this->_relationships as $relationship) {
            //echo "Saving " .get_class($relationship) . "<br />";
            $relationship->save();
        }

        
        $this->_data = array_merge($this->_data, $this->_changes);
        $this->_changes = array();
    }
    
    public function getColumns()
    {
        return $this->_meta->getColumns();
    }

    public function listRelationships()
    {
        return $this->_meta->listRelationships();
    }
    
    public function getName()
    {
        $exploded = explode('_', get_class($this));
        return strtolower(array_pop($exploded)); 
    }
    
    public function idColumn()
    {
        $columns = $this->getColumns();
        return $columns[$this->getName() . '_id'];
    }
    
    public function accept(PipesImpl_Visitor $visitor)
    {
        return $visitor->visit($this);
    }
}
